/*
** Copyright (c) 2008 Mobile GCalendar
**
** Permission is hereby granted, free of charge, to any person
** obtaining a copy of this software and associated documentation
** files (the "Software"), to deal in the Software without
** restriction, including without limitation the rights to use,
** copy, modify, merge, publish, distribute, sublicense, and/or sell
** copies of the Software, and to permit persons to whom the
** Software is furnished to do so, subject to the following
** conditions:
**
** The above copyright notice and this permission notice shall be
** included in all copies or substantial portions of the Software.
**
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
** OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
** NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
** HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
** WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
** OTHER DEALINGS IN THE SOFTWARE.
*/

package au.edu.mq.comp.itec800.mgc;

import java.io.FileNotFoundException;
import java.util.HashMap;
import java.util.Map;

import org.apache.log4j.Logger;

import au.edu.mq.comp.itec800.mgc.lib.GAccount;
import au.edu.mq.comp.itec800.mgc.lib.GCalendarService;
import au.edu.mq.comp.itec800.mgc.ui.UserInterface;
import au.edu.mq.comp.itec800.mgc.ui.cli.Console;
import au.edu.mq.comp.itec800.mgc.ui.cli.Context;
import au.edu.mq.comp.itec800.mgc.ui.cli.ICommand;
import au.edu.mq.comp.itec800.mgc.ui.cli.commands.MainCommand;
import au.edu.mq.comp.itec800.mgc.ui.gui.Application;
import au.edu.mq.comp.itec800.mgc.util.CommandLine;
import au.edu.mq.comp.itec800.mgc.util.Configuration;
import au.edu.mq.comp.itec800.mgc.util.MGCException;
import au.edu.mq.comp.itec800.mgc.util.OptionParser;
import au.edu.mq.comp.itec800.mgc.util.MGCException.Severity;

/**
 * Main class of the Mobile GCalendar client software.
 *
 * @author Laurent Malvert [laurent.malvert@students.mq.edu.au]
 */
public class MGC implements Runnable
{
  private static final Logger       logger      = Logger.getLogger(MGC.class);

  public static final String        PROJECT     = "mq-itec800-2008-1";
  public static final String        APPLICATION = "mgc";
  public static final String        VERSION     = "0.3";
  public static final String        REVISION    = "$Revision: 573 $";

  protected static MGC              self        = null;

  private Configuration             config      = null;
  private Context                   context     = Context.main;
  private UserInterface             ui          = null;
  private HashMap<String, GAccount> gaccounts   = new HashMap<String, GAccount>();
  private GAccount                  aga         = null;


  private MGC()
  {
    this(null);
  }

  private MGC(final String[] args)
  {
    self = this; // already setting the singleton
    init();
    parseOptions(args);
    ui = getConfiguration().check("ui", "cli") ? initConsole() : initGUI();
  }

  private void                  init()
  {
    logger.debug(version());
    logger.debug(GCalendarService.version());
    logger.info(" + initializing");
    GCalendarService.setApplicationName(revision());
    try
    {
      initConfiguration();
    }
    catch (final MGCException e)
    {
      // this is a non-fatal exception, keep going
      logger.warn(e.getMessage(), e);
    }
  }

  private void                  initConfiguration()
    throws MGCException
  {
    logger.info("   + initializing configuration manager");
    try
    {
      getConfiguration().loadFromXML("config/mgc.xml");
    }
    catch (final Configuration.ConfigurationException e)
    {
      if ((e.getCause() != null) &&
          (e.getCause() instanceof FileNotFoundException))
      {
        logger.warn("     ! no configuration file found. using defaults.");
      }
      else
      {
        throw new MGCException("Configuration Error:" + e.getMessage(),
                               e,
                               Severity.Warning);
      }
    }
  }

  private Console               initConsole()
  {
    logger.info("   + initializing console mode user interface");
    return (new Console() {
      @Override
      public String       prompt(final String format, final Object ... args)
      {
        final GAccount    aa  = getActiveAccount();

        return (String.format("[%s] %s> ",
                              ((aa != null) ? aa.getUser() : "anonymous"),
                              getContext().name()));
      }

      public boolean      exec(final ICommand cmd, final String[] args)
        throws MGCException
      {
        boolean           status  = false;

        if (cmd != null)
        {
          if ((getActiveAccount() != null) ||
              (cmd.needsAuthentication() == false))
          {
            status = cmd.execute(args);
          }
          else
          {
            getIO().output("[ERROR] this command requires authentication\n");
          }
        }
        return (status);
      }

      @Override
      public void         process(final String line)
      {
        final CommandLine cmd = CommandLine.load(line);

        try
        {
          logger.info(" > trying to execute: " + cmd.toString());
          exec(getContext().call(cmd.getCommand()), cmd.getArguments());
        }
        catch (final MGCException e)
        {
          logger.error(e.getMessage(), e);
          if (e.getSeverity() == MGCException.Severity.Fatal)
          {
            getIO().output("[ERROR] a fatal error occured." +
                           "         %s\n        exiting MGC...\n");
            stop();
          }
        }
        catch (final RuntimeException e)
        {
          logger.error(e.getMessage(), e);
          getIO().output("[ERROR] an unpredictable exception occured." +
                         "         %s\n        exiting MGC...\n",
                         e.getMessage());
          stop();
        }
      }
    });
  }

  private Application           initGUI()
  {
    logger.info("   + initializing android user interface");
    return (new Application());
  }

  public void                   parseOptions(final String[] args)
  {
    if ((args != null) && (args.length > 0))
    {
      logger.info("   + parsing command line arguments");
      new OptionParser(args);
    }
    logger.debug(" + Options: " + getConfiguration().toString());
  }

  /**
   *
   */
  private void                  cleanup()
  {
    logger.info(" + cleaning up");
    /*
     * TODO:  implement program way-out cleanup here
     *        this could be used to finish an ongoing synchronization
     *        or severe database/network connections
     */
  }

  /**
   * Main process loop, executed in the Console or the GUI.
   * We perform some cleanup before leaving and null-ing our
   * own reference to the MGC object just in case.
   *
   * The first authentication is performed or requested at this point.
   * (the key-retrieval for "user" and "password" are properties are
   * "unsafe" on purpose, as MainCommand.user.login(String, String) accepts
   * null values.
   *
   * @see java.lang.Runnable#run()
   */
  public void                   run()
  {
    logger.info(" + running");
    MainCommand.user.execute(getConfiguration().getProperty("user"),
                             getConfiguration().getProperty("password"));
    for (final String query : getConfiguration().getArguments())
    {
      ((Console) getUI()).process(query);
    }
    if (getConfiguration().check("batch", "false"))
    {
      getUI().run();
    }
    cleanup();
    self = null;
    logger.info(" + quitting");
  }


  /**
   * Returns a revision string with the following pattern:
   *  r<REVISION_NUMBER>
   * like:
   *  r567
   * If the REVISION public static variable isn't set, a default
   * value is used for parsing.
   *
   * @return
   *        the revision String object
   */
  public static String          revision()
  {
    final String  rev = (REVISION != null) ? REVISION : "$Revision: 573 $";
    return ("r" + rev.substring(11, rev.lastIndexOf("$")));
  }

  /**
   * Returns a version string with the following pattern:
   *  PROJECT APPLICATION VERSION.REVISION
   * like:
   *  mq-itec800-2008-1 mgc 0.1.r567
   *
   *
   * @return
   *        the version String object
   */
  public static String          version()
  {
    return (PROJECT + " " + APPLICATION + " " + VERSION + "." + revision());
  }

  public Map<String, GAccount>  getGoogleAccounts()
  {
    return (gaccounts);
  }

  /**
   * Returns the currently active GAccount object, representing the
   * Google Account currently connected to the Google Calendar Service.
   *
   * Returns null if no user is authenticated yet.
   *
   * @return
   *          the active GAccount object.
   */
  public GAccount               getActiveAccount()
  {
    return (aga);
  }

  public void                   setActiveAccount(final GAccount ga)
  {
    if (ga != null)
    {
      aga = ga;
    }
  }

  /**
   * Returns the MGC process' Configuration object.
   *
   * This Configuration object is being lazy instantiated here, and its
   * default values for the MGC program are being set.
   *
   * The loadFromXML(String file) method is called later on during
   * the MGC program execution to load settings from the configuration
   * file and overwrite the default values if necessary.
   *
   * @return
   *          the Configuration object.
   */
  public Configuration          getConfiguration()
  {
    if (config == null)
    {
      logger.info("     + setting defaut configuration options");
      config = new Configuration();
      config.setDefault("ui", "cli");
      config.setDefault("batch", "false");
    }
    return (config);
  }

  /**
   * Returns the UserInterface's subclass object initialized and used
   * by the current MGC process.
   *
   * @return
   *          the UserInterface subclass object.
   */
  public UserInterface          getUI()
  {
    return (ui);
  }

  /**
   * Returns the current active context.
   *
   * @return
   *          the Context object.
   */
  public Context                getContext()
  {
    return (context);
  }

  /**
   * Switches the current active context to the one given as a parameter.
   *
   * @param context
   *          the Context object to switch to.
   */
  public void                   setContext(final Context context)
  {
    this.context = context;
  }

  /**
   * Singleton initializer for MGC objects.
   *
   * @return
   *          the currently active MGC instance.
   */
  public static MGC             getInstance()
  {
    return (getInstance(null));
  }


  /**
   * Singleton initializer for MGC objects,
   * conveying taking option parameters for first-time instantiation.
   *
   * @return
   *          the currently active MGC instance.
   */
  public static MGC             getInstance(final String[] args)
  {
    if (self == null)
    {
      self = new MGC(args);
    }
    return (self);
  }

  static class Options
  {
    private final HashMap<String, String> options = new HashMap<String, String>();


    public Options()
    {

    }

    public static Options parse(final String ... args)
    {
      final Options       opts = new Options();

      for (final String arg : args)
      {
        if (arg != null)
        {
          final String    carg  = arg.trim();

          if (carg.startsWith("--"))
          {
            final String[]  opt   = arg.trim().split("=", 2);

            if (opt.length > 0)
            {
              opts.set(opt[0], (opt.length > 1) ? opt[1] : "true");
            }
          }
        }
      }
      return (opts);
    }

    public boolean        has(final String key)
    {
      return (options.containsKey(key));
    }

    public String         get(final String key)
    {
      return (options.get(key));
    }

    public String         set(final String key, final String value)
    {
      return (options.put(key, value));
    }
  }

  /**
   * MGC's main entry point.
   *
   * @param args
   *              array of command-line arguments for the MGC program.
   */
  public static void            main(final String[] args)
  {
    MGC.getInstance(args).run();
  }
}
